/******************************************************************************* * Copyright 2011 Google Inc. All Rights Reserved. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.google.gwt.eclipse.core.validators.java; import com.google.gdt.eclipse.core.JavaASTUtils; import com.google.gwt.dev.jjs.Correlation; import com.google.gwt.dev.jjs.Correlation.Axis; import com.google.gwt.dev.jjs.CorrelationFactory; import com.google.gwt.dev.jjs.InternalCompilerException; import com.google.gwt.dev.jjs.SourceInfo; import com.google.gwt.dev.jjs.SourceOrigin; import com.google.gwt.dev.js.JsParser; import com.google.gwt.dev.js.JsParserException; import com.google.gwt.dev.js.JsParserException.SourceDetail; import com.google.gwt.dev.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsExprStmt; import com.google.gwt.dev.js.ast.JsFunction; import com.google.gwt.dev.js.ast.JsNameRef; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsStatement; import com.google.gwt.dev.js.ast.JsVisitor; import com.google.gwt.eclipse.core.GWTPluginLog; import com.google.gwt.eclipse.core.editors.java.GWTPartitions; import com.google.gwt.eclipse.core.markers.GWTJavaProblem; import com.google.gwt.eclipse.core.markers.GWTProblemType; import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DefaultLineTracker; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextUtilities; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; /** * Parses JSNI blocks and collects all Java references. */ public final class JsniParser { /** * Lightweight version of {@link JsParserException} that contains only the * information we need for the Problems view (message, doc offset). */ @SuppressWarnings("serial") public static class JavaScriptParseException extends Exception { private final int offset; public JavaScriptParseException(String message, int offset) { super(message); this.offset = offset; } public int getOffset() { return offset; } } /** * Default implementation for {@link SourceInfo}, which is now required by * {@link JsParser}. We actually don't need any of the functionality that it * provides; it is only needed by GWT's compiler tools, such as "Story of Your * Compile" and "Web-Mode Stack Traces". */ @SuppressWarnings("serial") private static class SourceInfoAdapter implements SourceInfo { @Override public void addCorrelation(Correlation c) { } public void copyMissingCorrelationsFrom(SourceInfo other) { } public List<Correlation> getAllCorrelations() { return Collections.emptyList(); } public List<Correlation> getAllCorrelations(Axis axis) { return Collections.emptyList(); } @Override public int getEndPos() { return -1; } @Override public String getFileName() { return "unknown"; } public Correlation getPrimaryCorrelation(Axis axis) { return null; } public Set<Correlation> getPrimaryCorrelations() { return Collections.emptySet(); } public Correlation[] getPrimaryCorrelationsArray() { return null; } @Override public int getStartLine() { return -1; } @Override public int getStartPos() { return -1; } public SourceInfo makeChild(Class<?> caller, String description) { return this; } public SourceInfo makeChild(Class<?> caller, String description, SourceInfo... merge) { return this; } public void merge(SourceInfo... sourceInfos) { } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#getCorrelation(com.google.gwt.dev.jjs.Correlation.Axis) */ @Override public Correlation getCorrelation(Axis axis) { // TODO(${user}): Auto-generated method stub return null; } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#getCorrelations() */ @Override public Correlation[] getCorrelations() { // TODO(${user}): Auto-generated method stub return null; } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#getCorrelator() */ @Override public CorrelationFactory getCorrelator() { // TODO(${user}): Auto-generated method stub return null; } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#getOrigin() */ @Override public SourceOrigin getOrigin() { // TODO(${user}): Auto-generated method stub return null; } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#makeChild() */ @Override public SourceInfo makeChild() { // TODO(${user}): Auto-generated method stub return null; } /* (non-Javadoc) * @see com.google.gwt.dev.jjs.SourceInfo#makeChild(com.google.gwt.dev.jjs.SourceOrigin) */ @Override public SourceInfo makeChild(SourceOrigin origin) { // TODO(${user}): Auto-generated method stub return null; } } public static final String JSNI_BLOCK_END = "}-*/"; public static final String JSNI_BLOCK_START = "/*-{"; private static final String JS_FUNCTION_FOOTER = "}"; private static final String JS_FUNCTION_HEADER = "function(){"; public static String extractMethodBody(String jsniMethod) { int startPos = jsniMethod.indexOf(JSNI_BLOCK_START); int endPos = jsniMethod.lastIndexOf(JSNI_BLOCK_END); if (startPos < 0 || endPos < 0) { return null; } startPos += JSNI_BLOCK_START.length(); return jsniMethod.substring(startPos, endPos); } public static ITypedRegion getEnclosingJsniRegion(ITextSelection selection, IDocument document) { try { ITypedRegion region = TextUtilities.getPartition(document, GWTPartitions.GWT_PARTITIONING, selection.getOffset(), false); if (region.getType().equals(GWTPartitions.JSNI_METHOD)) { int regionEnd = region.getOffset() + region.getLength(); int selectionEnd = selection.getOffset() + selection.getLength(); // JSNI region should entirely contain the selection if (region.getOffset() <= selection.getOffset() && regionEnd >= selectionEnd) { return region; } } } catch (BadLocationException e) { GWTPluginLog.logError(e); } return null; } public static JavaValidationResult parse(MethodDeclaration method) { final JavaValidationResult result = new JavaValidationResult(); try { try { // Find all Java references result.addAllJavaRefs(findJavaRefs(method)); // Validate the Java references for (JsniJavaRef ref : result.getJavaRefs()) { GWTJavaProblem problem = validateJavaRef(method, ref); if (problem != null) { result.addProblem(problem); } } } catch (JavaScriptParseException e) { // Add the offset of the method declaration to get a document offset int offset = e.getOffset() + method.getStartPosition(); // Add the problem as a 1 character wide error, since we don't get the // length of the error "region" from JsParser result.addProblem(GWTJavaProblem.create(method, offset, 1, GWTProblemType.JSNI_PARSE_ERROR, e.getMessage())); } catch (IOException e) { GWTPluginLog.logError(e, "IO error while parsing JSNI method " + method.getName()); } catch (InternalCompilerException e) { String errorMsg = "Unexpected error parsing JSNI method " + method.getName() + ": " + e.getMessage(); Throwable cause = (e.getCause() != null ? e.getCause() : e); GWTPluginLog.logError(cause, errorMsg); } } catch (BadLocationException e) { GWTPluginLog.logError(e, "Error translating JS parse error location in " + method.getName()); } return result; } public static JsBlock parse(String jsniMethod) throws IOException, BadLocationException, JavaScriptParseException { String jsni = extractMethodBody(jsniMethod); if (jsni == null) { // TODO: if this file is client code (and if we got into JsniParser, it // should be), warn about missing JSNI block? return null; } // Wrap JavaScript code in a fake function for parsing jsni = JS_FUNCTION_HEADER + jsni + JS_FUNCTION_FOOTER; try { return parseFunctionBlock(jsni, 0); } catch (JsParserException e) { // Calculate the offset of the error within the fake JS function SourceDetail source = e.getSourceDetail(); DefaultLineTracker lineTracker = new DefaultLineTracker(); lineTracker.set(jsni); int offset = lineTracker.getLineOffset(source.getLine()) + source.getLineOffset() - 1; // Calculate the offset within the original JSNI method declaration offset -= JS_FUNCTION_HEADER.length(); offset += jsniMethod.indexOf(JSNI_BLOCK_START); offset += JSNI_BLOCK_START.length(); // The errors we get back from JsParser tend to be all lower case, so // for consistency with JDT errors we always capitalize the first char String errorMessage = e.getMessage(); if (errorMessage != null && errorMessage.length() > 0) { char[] errorChars = e.getMessage().toCharArray(); errorChars[0] = Character.toUpperCase(errorChars[0]); errorMessage = new String(errorChars); } // Rethrow a new exception with our fixed up source location throw new JavaScriptParseException(errorMessage, offset); } catch (NullPointerException e) { // This is b/c a deeper function is throwing an NPE // TODO workaround for exception being thrown. // TODO what should really happen here? // TODO appears that single line jsni statements are thrown as npe //System.out.println("JSNI=" + jsni); return null; } } private static List<JsniJavaRef> findJavaRefs( final MethodDeclaration jsniMethod) throws IOException, JavaScriptParseException, BadLocationException { final String jsniSource = JavaASTUtils.getSource(jsniMethod); ICompilationUnit cu = JavaASTUtils.getCompilationUnit(jsniMethod); final IPath cuPath = cu.getResource().getFullPath(); final List<JsniJavaRef> javaRefs = new ArrayList<JsniJavaRef>(); JsBlock js = JsniParser.parse(jsniSource); if (js != null) { // Visit the JavaScript AST to find all Java references new JsVisitor() { @Override public void endVisit(JsNameRef x, @SuppressWarnings("rawtypes") JsContext ctx) { String ident = x.getIdent(); if (ident.indexOf("@") != -1) { JsniJavaRef javaRef = JsniJavaRef.parse(ident); if (javaRef != null) { // Set the reference's Java source file javaRef.setSource(cuPath); // To get the Java reference offset, we have to do an indexOf on // its identifier. To make sure we catch multiple references to // the same Java element, we need to start at the index one past // the start of the last Java reference we found (if any) int fromIndex = 0; if (javaRefs.size() > 0) { fromIndex = javaRefs.get(javaRefs.size() - 1).getOffset() - jsniMethod.getStartPosition() + 1; } int offset = jsniSource.indexOf(ident, fromIndex) + jsniMethod.getStartPosition(); // Set the reference's offset within the Java source file javaRef.setOffset(offset); javaRefs.add(javaRef); } } } }.accept(js); } return javaRefs; } @SuppressWarnings("serial") private static JsBlock parseFunctionBlock(String js, final int startLine) throws JsParserException, IOException, NullPointerException { JsProgram jsPgm = new JsProgram(); StringReader r = new StringReader(js); List<JsStatement> stmts = JsParser.parse(new SourceInfoAdapter() { @Override public int getStartLine() { return startLine; } }, jsPgm.getScope(), r); // Rip the body out of the parsed function and attach the JavaScript // AST to the method. // JsFunction fn = (JsFunction) ((JsExprStmt) stmts.get(0)).getExpression(); return fn.getBody(); } private static GWTJavaProblem validateJavaRef(MethodDeclaration jsniMethod, JsniJavaRef ref) { ICompilationUnit cu = JavaASTUtils.getCompilationUnit(jsniMethod); try { ref.resolveJavaElement(cu.getJavaProject()); return null; } catch (UnresolvedJsniJavaRefException e) { // A null problem type indicates that we should ignore the unresolved // reference. This happens, for example, on @null::nullMethod(). if (e.getProblemType() == null) { return null; } // If we did find an unresolved Java reference, return a problem marker // for it with the appropriate type and error message int offset = 0, length = 0; String[] messageArgs = new String[0]; switch (e.getProblemType()) { case JSNI_JAVA_REF_UNRESOLVED_TYPE: offset = ref.getClassOffset(); length = ref.className().length(); messageArgs = new String[] {ref.className()}; break; case JSNI_JAVA_REF_NO_MATCHING_CTOR: offset = ref.getMemberOffset(); length = ref.memberName().length(); messageArgs = new String[] { ref.readableMemberSignature(), ref.simpleClassName()}; break; case JSNI_JAVA_REF_MISSING_METHOD: offset = ref.getMemberOffset(); length = ref.memberName().length(); messageArgs = new String[] {ref.simpleClassName(), ref.memberName()}; break; case JSNI_JAVA_REF_NO_MATCHING_METHOD: offset = ref.getMemberOffset(); length = ref.memberName().length(); messageArgs = new String[] { ref.readableMemberSignature(), ref.simpleClassName()}; break; case JSNI_JAVA_REF_MISSING_FIELD: offset = ref.getMemberOffset(); length = ref.memberName().length(); messageArgs = new String[] {ref.className(), ref.memberName()}; break; default: assert (false); return null; } return GWTJavaProblem.create(jsniMethod, offset, length, e.getProblemType(), messageArgs); } } private JsniParser() { // Not instantiable } }